
/*
 * TODO:
 * 	- Validate options object and configuration
 * 	- Add support for repeat arguments
 * 	- Support list type arguments
 * 	- Support optional arguments
*/

/*
 * Intro
 * 
 * Very simple implementation of an option parser. It supports long and short
 * options, bundled options (-avd) and options with values. It receives an
 * argument object describing the options, their types (not to be confused with
 * their value types, i.e Integer, String, etc), a default value (if any), and,
 * possibly, functions for value validation and convertion. It returns an object
 * contanining key-value pairs of the defined long options.
 *
 * Options of type 'flag' will have their values set to 'true' if the option is
 * found at the command-line, otherwise they will be 'null'.
 *
 * This 'getOpt' also forces the definition of long option names, this is in
 * attempt to make the returned option object more readable. But nothing is
 * stopping the user from defining single letter long option names (i.e '--l').
 *
 * It also has optiona support for "bundling" (by default is turned on) both
 * options and option arguments. To turn bundling off, pass an object containing
 * { bundling: false } as the second argument. 
 *
 * This implementation tests each entry of the bundled argument for the type
 * 'flag'. Each matching letter is parsed as an 'flag' option until until a
 * 'value' type option is found, at which point the remainder of the option is
 * evaluated as an argument.
 *
 * Brief example
 *
 * { 
 * 	'has-value': 
 * 	{ 	
 * 		type: 'value', 
 * 		short: 'h', 
 * 		convert: v => parseInt(v),
 * 		validate: v => v > 1
 * 	},
 * 	verbose: { 
 * 		type: 'flag', 
 * 		short: 'v' 
 * 	}
 * }
 *
 * If a command with the options of '--has-value 5 --verbose' or '-h 5 -v' was
 * supplied, it would generate and the options object: { 'has-value': 5,
 * verbose: true }.
 *
 * One could also make use of bundling and supply a the argument: '-vh5', which
 * produce the same object.
 *
 * Argument Object
 *
 * 		type
 *
 * There are two types of arguments supported: arguments with user defined
 * values ('type: value') and arguments that act as switches ('type: flag') and
 * carry no user specified value. They have two strategies for parsing: type
 * 'value' consumes the next immediate argument; and type 'flag' doesn't consume
 * anything on the argument parsing loop.
 *
 * 		short 
 *
 * A short version of the option, meant to be used with a single dash ('-')
 * character. Short options can be bundled together ('-abc' or '-p3000'), but
 * only if they are all of type 'flag', or if the there is a single 'value' type
 * option on the end ('-abp3000').
 *
 * 		default
 *
 * Default value for option in case none is specified.
 *
 * 		convert
 *
 * A optional function to perform any required operations on the extracted
 * value. Its executed before the 'validate' function.
 *
 * 		validate
 *
 * An optional function that tests the value of value. Its exectued after the
 * 'convert' function and must return a falsy or truthy value.
 *
 * Quirks
 *
 * An option that requires an argument, but that is immediately followed by
 * another option, will have this option set at its argument.
*/

"use strict";

import { die } from './die.js';

export function getOpt(argObj, config) {
	const optKeys = Object.keys(argObj);
	let argv = process.argv.slice(2)
	let opts = {};
	let notOpts = [];
	
	let bundle = true;
	bundle = config.bundle;
	
	let arg;
	while ((arg = argv.shift())) {
		// Skip non-option args
		if (!arg.startsWith('-')) {
			notOpts.push(arg);
			continue;
		}
		
		// End of options
		if (arg == '--') break;

		let argOpt;
		let argKey;
		if (arg.startsWith('--')) { // long option
			argOpt = arg.slice(2);
			argKey = optKeys.find(k => k == argOpt);
			if (!argKey) 
				die(`Unrecognized option: ${arg}`);
		} else { // short option
			argOpt = arg.slice(1);
			const opt = argOpt[0];
			const key = optKeys.find(k => argObj[k].short == opt);

			if (!key) 
				die(`Unrecognized option '${argOpt}'`);
			if (Object.keys(opts).includes(key))
				die(`Option '-${opt}' already specified`)
			
			argKey = key;
			
			if (argOpt.length > 1) {
				// If bundling, either schedule remainder to be parsed as
				// options or arguments dependinng or option type
				if (bundle) {
					let asOpt = '-';
					if (argObj[argKey].type == 'value') asOpt = '';
					argv.unshift(`${asOpt}${argOpt.slice(1)}`);
				} else 
					die(`Unrecognized option: '${arg}'`);
			}
		}
		
		const type = argObj[argKey].type;
		
		switch (type) {
		case 'value':
			let val = argv.shift();
			
			const validateFunc = argObj[argKey].validate;
			const convertFunc = argObj[argKey].convert;
			if (convertFunc) val = convertFunc(val)
			if (validateFunc) {
				if (!validateFunc(val)) throw '';
			}
			opts[argKey] = val;
			break;
		case 'flag':
			opts[argKey] = true;
			break;
		default:
			die(`Invalid ${type} for key ${argKey}`);
		}
	}
	if (notOpts.length >= 1)
		process.argv = process.argv.slice(0, 2).concat(notOpts);

	// Fill empty options with their default values
	Object.keys(argObj).forEach(k => {
		if (!opts[k]) {
			if (argObj[k].default)
				opts[k] = argObj[k].default;
			else opts[k] = null;
		}
	});
	return opts;
}
